package cn.kivensoft.util;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HttpUtils {
	public static final String GET = "GET";
	public static final String POST = "POST";
	public static final String PUT = "PUT";
	public static final String DELETE = "DELETE";

	private static final String BOUNDARY_SPLIT = "--";
	private static final String NL = "\r\n";

	private HttpUtils() {}

	/** get请求访问http的api函数
	 * @param url 访问地址
	 * @return 访问结果
	 */
	public static HttpResponse get(String url) throws IOException {
		return get(makeUrl(url, null), null);
	}

	/** get请求访问http的api函数
	 * @param url 访问地址
	 * @param urlParam URL参数
	 * @param headers 请求头部定制内容
	 * @return 访问结果
	 */
	public static HttpResponse get(String url, Object urlParam, Map<String, String> headers) throws IOException {
		return get(makeUrl(url, urlParam), headers);
	}

	/** get请求访问http的api函数
	 * @param url 访问地址
	 * @param headers 请求头部内容
	 * @return 请求结果文本内容
	 */
	public static HttpResponse get(String url, Map<String, String> headers) throws IOException {
		return request(GET, url, null, headers);
	}
	
	/** post请求访问http的api函数
	 * @param url 访问地址
	 * @param param 提交的内容对象
	 * @return 访问结果
	 */
	public static HttpResponse post(String url, Object param) throws IOException {
		return post(url, param, null, null);
	}

	/** post请求访问http的api函数
	 * @param url 访问地址
	 * @param param 提交的内容对象
	 * @param urlParam url参数
	 * @param headers 请求头部定制内容
	 * @return 访问结果
	 */
	public static HttpResponse post(String url, Object param, Object urlParam, Map<String, String> headers) throws IOException {
		return post(makeUrl(url, urlParam), param, headers);
	}

	/** post请求访问http的api函数
	 * @param url 访问地址
	 * @param param 提交的内容对象
	 * @param headers 请求头部定制内容
	 * @return 访问结果
	 */
	public static HttpResponse post(String url, Object param, Map<String, String> headers) throws IOException {
		// 编码请求参数
		byte[] body;
		if (param != null) {
			if (param instanceof String)
				body = Strings.toBytes((String) param);
			else if (param instanceof byte[])
				body = (byte[]) param;
			else
				body = Fmt.get().appendJson(param).toBytes();
		} else {
			body = null;
		}

		return request(POST, url, body, headers);
	}

	/** post请求访问http的api函数
	 * @param url 访问地址
	 * @param param 参数，采用post form-data方式提交
	 * @return 访问结果
	 */
	public static HttpResponse postFormData(String url, Object param, Map<String, String> headers) throws IOException {
		// 编码请求参数
		byte[] body = Strings.toBytes(encodeParam(param));

		HashMap<String, String> newHeaders = new HashMap<>();
		if (headers != null) newHeaders.putAll(headers);
		if (!newHeaders.containsKey("Content-Type"))
			newHeaders.put("Content-Type", "application/x-www-form-urlencoded");

		return request(POST, url, body, newHeaders);
	}
	
	/** 上传文件对象 */
	public static class UpFile {
		public String name;
		public String filename;
		public InputStream inputStream;
		
		public UpFile(String name, String filename, InputStream inputStream) {
			this.name = name;
			this.filename = filename;
			this.inputStream = inputStream;
		}
		
		public UpFile(String name, File file) throws FileNotFoundException {
			this.name = name;
			this.filename = file.getName();
			this.inputStream = new FileInputStream(file);
		}
	}

	/** post上传文件
	 * @param url 访问地址
	 * @param key 文件参数名
	 * @param file 文件名
	 * @param headers 请求头
	 * @return 上传结果
	 * 
	 */
	public static HttpResponse postFile(String url, String key, String file, Map<String, String> headers) throws IOException {
		return postFile(url, null, new UpFile[] {new UpFile(key, new File(file))}, headers);
	}
	
	/** post上传文件
	 * @param url 访问地址
	 * @param param 参数
	 * @param file 上传文件
	 * @param headers 请求头
	 * @return 上传结果
	 * 
	 */
	public static HttpResponse postFile(String url, Object param, UpFile file, Map<String, String> headers) throws IOException {
		return postFile(url, param, new UpFile[] {file}, headers);
	}
	
	/** post上传文件
	 * @param url 访问地址
	 * @param param 参数
	 * @param files 上传文件数组
	 * @param headers 请求头
	 * @return 上传结果
	 */
	@SuppressWarnings("unchecked")
	public static HttpResponse postFile(String url, Object param, UpFile[] files, Map<String, String> headers) throws IOException {
		String boundary = Fmt.concat("--------", Strings.uuid());
		HttpURLConnection conn = null;
		try {
			URL urlObj = new URL(url);
			conn = (HttpURLConnection) urlObj.openConnection();
			conn.setUseCaches(false);
			conn.setRequestMethod(POST);
			conn.setRequestProperty("Charset", "UTF-8");
			conn.setRequestProperty("Accept", "*/*");
			conn.setRequestProperty("Content-Type", Fmt.concat("multipart/form-data;boundary=", boundary));

			if (headers != null)
				for (Map.Entry<String, String> entry : headers.entrySet())
					conn.setRequestProperty(entry.getKey(), entry.getValue());

			conn.setDoInput(true);
			conn.setDoOutput(true);
			conn.connect();

			OutputStream os = conn.getOutputStream();
			
			// 写入参数
			if (param != null) {
				Fmt f = Fmt.get();
				if (param instanceof Map)
					for (Map.Entry<String, Object> entry : ((Map<String, Object>) param).entrySet())
						writeMultipartValue(f, boundary, entry.getKey(), entry.getValue());
				else
					BeanUtils.getFields(param, (n, v, c) -> {
						writeMultipartValue(f, boundary, n, v);
						return true;
					});

				f.output(os);
				os.flush();
			}
			
			// 写入上传文件
			for (UpFile file : files)
				writeMultipartFile(os, boundary, file);

			// 写入结束分隔符
			Fmt.get().append(BOUNDARY_SPLIT).append(boundary).append(BOUNDARY_SPLIT).output(os);
			os.flush();

			// 获取返回值
			int code = conn.getResponseCode();
			InputStream inputStream = code == 200 || code == 201 || code == 202 ? conn.getInputStream() : conn.getErrorStream();
			return new HttpResponse(conn.getResponseCode(), conn.getResponseMessage(),
					conn.getContentType(), conn.getHeaderFields(), readFrom(inputStream));
		} finally {
			if (conn != null) conn.disconnect();
		}
	}
	
	private static void writeMultipartValue(Fmt out, String boundary, String key, Object value) {
		if (value != null) {
			out.append(BOUNDARY_SPLIT).append(boundary).append(NL)
				.append("Content-Disposition: form-data; name=\"").append(key).append('"').append(NL)
				.append(NL)
				.append(value).append(NL);
		}
	}
	
	private static void writeMultipartFile(OutputStream out, String boundary, UpFile file) throws IOException {
		if (file.inputStream != null) {
			Fmt.get().append(BOUNDARY_SPLIT).append(boundary).append(NL)
				.append("Content-Disposition: form-data; name=\"").append(file.name)
				.append("\"; filename=\"").append(file.filename).append('"').append(NL)
				.append("Content-Type: application/octet-stream").append(NL)
				.append("Content-Transfer-Encoding: binary").append(NL)
				.append(NL)
				.output(out);
			IoUtils.transTo(file.inputStream, out, false);
			out.write(NL.getBytes(StandardCharsets.UTF_8));
			out.flush();
		}
	}
	
	private static String makeUrl(String url, Object param) {
		// 编码请求参数
		String req = param == null ? null : encodeParam(param);
		return req == null ? url : Fmt.concat(url, "?", req);
	}
	
	@SuppressWarnings("unchecked")
	public static String encodeParam(Object param) {
		if (param == null) return null;
		Fmt f = Fmt.get();
		// Map类型处理
		if (param instanceof Map)
			for (Map.Entry<String, Object> entry : ((Map<String, Object>) param).entrySet())
				encodeValue(entry.getKey(), entry.getValue(), entry.getValue().getClass(), f);
		// Object类型处理
		else
			BeanUtils.getFields(param, (name, value, valueClass) -> {
				encodeValue(name, value, valueClass, f);
				return true;
			});

		if (f.length() > 0) f.setLength(f.length() - 1);
		return f.length() == 0 ? null : f.release();
	}
	
	private static void encodeValue(String name, Object value, Class<?> valueClass, Fmt f) {
		if (value == null) return;

		int len = f.length();
		try {
			if (List.class.isAssignableFrom(valueClass)) {
				String nameEnc = URLEncoder.encode(name, Strings.UTF8);
				for (Object v : (List<?>) value)
					f.append(nameEnc).append('=').append(URLEncoder.encode(v.toString(), Strings.UTF8)).append('&');
			} else {
				f.append(URLEncoder.encode(name, Strings.UTF8)).append('=');
				
				if (valueClass == String.class && !((String)value).isEmpty())
					f.append(URLEncoder.encode((String)value, Strings.UTF8));
				else if (valueClass.isEnum())
					f.append(URLEncoder.encode(((Enum<?>)value).name(), Strings.UTF8));
				else
					f.append(value.toString());

				f.append('&');
			}
		} catch (UnsupportedEncodingException e) {
			f.setLength(len);
		}
	}
	
	/** post请求访问http的api函数
	 * @param method 请求类型, GET/POST/PUT/DELETE
	 * @param url 访问地址
	 * @param body 要提交的内容
	 * @param headers 请求头部定制内容
	 */
	public static HttpResponse request(String method, String url, byte[] body, Map<String, String> headers) throws IOException {
		HttpURLConnection conn = null;
		try {
			int bodyLen = body == null ? 0 : body.length;

			URL urlObj = new URL(url);
			conn = (HttpURLConnection) urlObj.openConnection();
			conn.setRequestMethod(method);
			conn.setRequestProperty("Accept", "*/*");
			if (!GET.equals(method)) {
				conn.setRequestProperty("Content-Length", String.valueOf(bodyLen));
				if (bodyLen > 0) {
					conn.setRequestProperty("Content-Type", "application/json");
					conn.setDoOutput(true);
				}
			}

			if (headers != null)
				for (Map.Entry<String, String> entry : headers.entrySet())
					conn.setRequestProperty(entry.getKey(), entry.getValue());

			conn.connect();

			// 发送POST请求必须设置如下两行
			if (!GET.equals(method) && bodyLen > 0)
				conn.getOutputStream().write(body);

			int code = conn.getResponseCode();
			InputStream inputStream = code == 200 || code == 201 || code == 202 ? conn.getInputStream() : conn.getErrorStream();
			return new HttpResponse(conn.getResponseCode(), conn.getResponseMessage(),
					conn.getContentType(), conn.getHeaderFields(), readFrom(inputStream));
		} finally {
			if (conn != null) conn.disconnect();
		}
	}

	private static byte[] readFrom(InputStream src) throws IOException {
		if (src == null) return new byte[0];

		BufferedInputStream bis = new BufferedInputStream(src);
		FastBuffer fb = new FastBuffer();
		BufChunk buf = fb.nextChunk();
		int count = bis.read(buf.data, buf.offset, buf.length);

		while (count > 0) {
			fb.setLength(fb.length() + count);
			buf = fb.nextChunk();
			count = bis.read(buf.data, buf.offset, buf.length);
		}

		return fb.getBytes();
	}
	
}
